<template>
  <div class="tinymce-editor">
    <editor
      :id="tinymceId"
      :key="tinymceKey"
      v-model="myValue"
      :init="init"
      :disabled="disabled"
    />
  </div>
</template>

<script>
import tinymce from "tinymce/tinymce";
import Editor from "@tinymce/tinymce-vue";

// 主题
import "tinymce/themes/silver/theme";
// 字体文件
import "tinymce/icons/default/icons";

// 常用插件
import "tinymce/plugins/image";
import "tinymce/plugins/link";
import "tinymce/plugins/media";
import "tinymce/plugins/table";
import "tinymce/plugins/lists";
import "tinymce/plugins/quickbars";
import "tinymce/plugins/preview";
import "tinymce/plugins/wordcount";
import "tinymce/plugins/colorpicker";
import "tinymce/plugins/fullscreen";
import "tinymce/plugins/code";

// 提取img标签正则
const imgReg = /<img.*?(?:>|\/>)/gi;

// 提取src正则
const srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i;

/**
 * 根据img的 src 内容获取id
 * @param {*} srcValue src内容
 */
const getValueFromSrc = function(srcValue) {
  return srcValue.slice(5).slice(0, -1);
};

/**
 * 将文件转为blob
 * @param {*} file 文件对象
 */
function getImgUrl(file) {
  return Promise.resolve()
    .then(() => URL.createObjectURL(file));
}

export default {
  name: "InputRich",
  components: {
    Editor
  },
  props: {
    value: {
      type: String,
      required: false,
      default: ""
    },
    // 富文本框编辑器id
    tinymceId: {
      type: [Number, String],
      default: "tinymceId"
    },
    disabled: {
      type: Boolean,
      default: false
    },
    width: {
      type: [Number, String],
      default: "100%"
    },
    skinUrl: {
      type: String,
      default: "/tinymce/css"
    },
    height: {
      type: Number,
      default: 400
    },
    minWidth: {
      type: Number,
      default: undefined
    },
    minHeight: {
      type: Number,
      default: undefined
    },
    maxWidth: {
      type: Number,
      default: undefined
    },
    maxHeight: {
      type: Number,
      default: undefined
    },
    // 调整富文本框宽高（false不能调整，true 只能调整高度，both 可调整宽高）
    resize: {
      type: [Boolean, String],
      default: true
    },
    // 插件
    plugins: {
      type: [String, Array],
      default: "lists image preview link media table wordcount fullscreen quickbars code"
    },
    // 工具栏
    toolbar: {
      type: [String, Array],
      default: "undo redo | fontsizeselect fontselect forecolor backcolor |  formatselect | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | lists link unlink image media table | removeformat code | clear preview fullscreen",
      branding: false
    },
    // 文件可上传类型
    fileType: {
      type: String,
      default: ".txt, .doc, .docx, .xls, .xlsx, .pdf, .ppt, .zip, .rar"
    },
    // 媒体文件可上传类型
    mediaType: {
      type: String,
      default: ".mp4"
    },
    // 上传图片大小限制（单位MB）
    imgMaxSize: {
      type: Number,
      default: 5
    },
    imgType: {
      type: String,
      default: ".jpg, .jpeg, .png, .bmp, .gif, .webp"
    },
    // 可进行选择上传的文件类型（默认为图片，视频和其他文件）
    filePickerType: {
      type: String,
      default: "image, media, file"
    },
    // 如果需要额外加载业务相关样式，传入 "/tinymce/css/home_setting.css" 这样的地址，文件放 public 下
    otherClass: {
      type: String,
      default: ""
    },
    // 初始字体大小和字体family
    defaultStyle: {
      type: Object,
      default: () => ({
        fontSize: "14px",
        fontFamily: "宋体"
      })
    },
    fontsizeFormats: {
      type: String,
      default: "8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 36pt"
    },
    fontFormats: {
      type: String,
      default: "黑体=黑体; 宋体=宋体; Arial=arial; serif=serif; sans-serif=sans-serif"
    },
    // 溢出工具显示形式，true 则一直显示，sliding为省略号，点击显示
    toolbarDrawer: {
      type: [Boolean, String],
      default: false
    },
    // 富文本内容行内样式
    contentStyle: {
      type: String,
      default: "img, table { max-width:100%; }"
    },
    // 是否需要对图片 / 文件通过下载回显
    needToken: {
      type: Boolean,
      default: true
    },
    // 根据图片id 下载图片函数
    downloadFn: {
      type: Function,
      default: null
    },
    // needToken为true 则返回 文件id，needToken为false 则返回拼接的url
    uploadFn: {
      type: Function,
      default: null
    },
    // 是否必填校验
    required: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      // 选择上传的文件类型选择范围
      limitFileType: {
        "image": this.imgType,
        "media": this.mediaType,
        "file": this.fileType
      },
      tinymceKey: new Date().getTime(),
      // 键为图片blob文件，值为 图片id，用于保存文件id以供选渲染时下载
      imgUrls: {},
      firstLoad: true,
      // 键为图片id，值为 blob文件，用于图片初始化渲染
      imgUrlsReverse: {},
      // 需要初始化的图片 id 集合
      initImgs: [],
      // 需要初始化图片数量
      initImgsLength: 0,
      // 初始化配置
      init: {
        language_url: "/tinymce/langs/zh_CN.js", // public文件夹下的中文翻译文件
        language: "zh_CN",
        width: this.width,
        height: this.height,
        min_width: this.minWidth,
        min_height: this.minHeight,
        max_width: this.maxWidth,
        max_height: this.maxHeight,
        object_resizing: true, // 图片/表格大小编辑器
        skin_url: this.skinUrl, // 加载富文本基本主题样式文件路径前缀
        content_css: this.otherClass, // public/tinymce文件夹下的自定义css样式文件
        plugins: this.plugins, // 插件
        toolbar: this.toolbar, // 工具类及其摆放位置顺序 值为false则隐藏工具栏
        branding: false, // 隐藏右下角文字
        menubar: false, // 显示 / 隐藏顶部菜单栏
        toolbar_drawer: this.toolbarDrawer, // 溢出工具显示形式，true 则一直显示，sliding为省略号，点击显示
        paste_data_images: false, // 粘贴图像
        image_advtab: true, // 图片高级参数
        image_uploadtab: false, // 图片上传不显示最后一个 tab - 上传，上传有 bug
        content_style: this.contentStyle, // 限制图片/表格最大宽度
        convert_urls: false, // 自动转换url
        resize: this.resize, // 调整宽高
        inline: false, // 非内联模式
        fontsize_formats: this.fontsizeFormats, // 文字大小调整可选择字号
        font_formats: this.fontFormats, // 字体选择
        quickbars_selection_toolbar: "removeformat | bold italic underline strikethrough | fontsizeselect forecolor backcolor", // 快速栏操作配置
        quickbars_insert_toolbar: !this.disabled, // 快速栏插入图片/表格，禁用时改为false进行禁用
        file_picker_types: this.filePickerType, // 可进行选择上传的文件类型
        /**
         * 文件上传处理
         */
        file_picker_callback: (callback, value, meta) => {
          // 判断文件类型是否为媒体文件
          const filetype = meta.filetype;
          // 模拟出一个input用于添加本地文件
          const input = document.createElement("input");
          input.setAttribute("type", "file");
          input.setAttribute("accept", this.limitFileType[filetype]);
          const that = this;
          input.onchange = function() {
            const file = this.files[0];
            if (filetype === "image") {
              // 上传图片处理
              if (file.size > that.imgMaxSize * 1024 * 1024) {
                that.$message.warning(`图片请不要大于 ${that.imgMaxSize}MB`);
                return;
              }
              if (that.needToken) {
                // 需要下载回显时，直接返回一个blob图片文件即可，它会触发 images_upload_handler 实现上传
                getImgUrl(file)
                  .then(v => {
                    callback(v, { text: file.name });
                  });
              } else {
                that.uploadFn(filetype, file)
                  .then(url => {
                    // 返回的拼接 url，直接回显
                    callback(url, { text: file.name });
                  });
              }
            } else {
              // 上传的视频 / 文件处理
              that.uploadFn(filetype, file)
                .then(file => {
                  let url = "";
                  if (that.needToken) {
                    // 需要通过下载，所以将url用 download=fileId:fileName形式保存
                    url = `download=${file}:${file.name}`;
                  } else {
                    // 否则不用考虑过多安全性，直接返回可下载地址url
                    url = file;
                  }
                  callback(url, { text: file.name });
                });
            }
          };
          input.click();
        },
        // 防止新上传图片时触发旧图片也上传
        images_replace_blob_uris: false,
        /**
         * 图片上传处理
         */
        images_upload_handler: async(blobInfo, success, failure) => {
          let file = blobInfo.blob();
          if (Object.prototype.toString.call(file) === "[object Blob]") {
            file = new File([file], `${new Date().getTime()}.${file.type.split("/")[1]}`, { type: file.type });
          }
          if (file.size > this.imgMaxSize * 1024 * 1024) {
            failure(`图片请不要大于 ${this.imgMaxSize}MB`);
          } else if (this.needToken) {
            const url = blobInfo.blobUri();
            // 先 resolve，避免 getContent 的时候 blob 被改为 base64
            success(url);
            if (this.imgUrls[url]) {
              return;
            }
            if (this.handleInitImg()) {
              return;
            }
            this.uploadFn("image", file)
              .then(id => {
                this.setImgCache(id, url);
              })
              .catch(() => {
                failure();
                // 上传失败了，需将刚刚的图片销毁
                const reg = `(<img.*?src=['"]${url}['"].*?>)`;
                this.myValue = this.myValue.replace(new RegExp(reg), "");
              });
          } else {
            this.uploadFn("image", file)
              .then(imgUrl => {
                success(imgUrl);
              });
          }
        },
        init_instance_callback: editor => {
          editor.on("paste", evt => {
            // 监听粘贴事件对粘贴图片进行处理
            this.onPaste(evt);
          });
          editor.on("blur", evt => {
            // 失焦时重新更新数据，避免存在历史 blob
            this.handleBlur(evt);
          });
        },
        setup: ed => {
          const _this = this;
          ed.on("init", function() {
            // eslint-disable-next-line no-invalid-this
            this.getDoc().body.style.fontSize = _this.defaultStyle.fontSize;
            // eslint-disable-next-line no-invalid-this
            this.getDoc().body.style.fontFamily = _this.defaultStyle.fontFamily;
          });
          // 自定义清空按钮事件
          ed.ui.registry.addButton("clear", {
            text: "清空",
            icon: "reload",
            onAction: () => {
              this.clear();
            }
          });
        }
      },
      myValue: this.value
    };
  },
  watch: {
    value: {
      handler(newValue, oldValue) {
        if (!oldValue && newValue && this.needToken && this.firstLoad) {
          // 第一次有值时加载图片
          this.handleInitValue(newValue);
          this.firstLoad = false;
        }
        this.myValue = (newValue === null ? "" : newValue);
      }
    },
    myValue(newValue) {
      this.$emit("input", newValue);
      this.$emit("change", newValue);
      if (this.required) {
        this.handleValidate(this);
      }
    },
    imgUrls: {
      handler(val) {
        this.$emit("img", val);
      },
      immediate: true
    }
  },
  mounted() {
    tinymce.init({});
  },
  activated() {
    // 用于页面使用 keep-alive 缓存后需要在激活时更新key 以便重新加载组件
    this.tinymceKey = new Date().getTime();
  },
  deactivated() {
    this.myValue = "";
    if (this.required) {
      this.clearValidate(this);
    }
  },
  methods: {
    /**
     * 清空内容时清空校验
     */
    clearValidate(node) {
      setTimeout(() => {
        // 使用定时器为了让 watch 监听myValue中逻辑先执行
        if (!node.$parent) {
          return;
        }
        const parent = node.$parent;
        if (parent.$vnode.tag.endsWith("ElFormItem") || parent.$el.className.includes("el-form-item")) {
          parent.clearValidate();
          return;
        }
        this.clearValidate(parent);
      }, 0);
    },
    /**
     * 递归遍历查找父级表单项元素触发校验
     */
    handleValidate(node) {
      if (!node.$parent) {
        return;
      }
      const parent = node.$parent;
      if (parent.$vnode.tag.endsWith("ElFormItem") || parent.$el.className.includes("el-form-item")) {
        parent.$emit("el.form.blur");
        return;
      }
      this.handleValidate(parent);
    },
    /**
     * 添加自定义事件--清空内容
     */
    clear() {
      this.$confirm("是否清空所有数据", "确认清空", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning"
      })
        .then(() => {
          this.myValue = "";
          this.$message.success("清空成功！");
        })
        .catch(() => {
          this.$message.warning("已取消！");
        });
    },
    /**
     * 粘贴处理
     */
    onPaste: function(event) {
      const items = (event.clipboardData || window.clipboardData).items;
      if (items?.[0]?.type.indexOf("image") !== -1) {
        // 进入此if说明粘贴内容为图片，单独处理
        event.preventDefault();
        const file = items[0].getAsFile();
        if (this.needToken) {
          // 这里不需要调用上传函数，因为会自动触发 images_upload_handler 进行上传并替换掉当前url
          getImgUrl(file)
            .then(v => {
              window.tinymce.get(this.tinymceId).insertContent(`<img src="${v}" >`);
            });
        } else {
          this.uploadFn("image", file)
            .then(imgUrl => {
              window.tinymce.get(this.tinymceId).insertContent(`<img src="${imgUrl}" >`);
            });
        }
      }
    },
    /**
     * 初始化图片
     */
    initModelImg(content) {
      return content.replace(imgReg, item => item.replace(srcReg, item => {
        const id = getValueFromSrc(item);
        if (item.indexOf("blob:http:") !== -1) {
          return item;
        }
        return `src="${this.imgUrlsReverse[id]}"`;
      }));
    },
    /**
     * 根据id下载图片
     */
    getImgById(id) {
      return this.downloadFn(id)
        .then(v => getImgUrl(v))
        .then(v => {
          this.setImgCache(id, v);
          return v;
        });
    },
    /**
     * 设置图片缓存键值对
     */
    setImgCache: function(id, url) {
      this.imgUrls[url] = id;
      this.imgUrlsReverse[id] = url;
    },
    /**
     * 初始化时如果存在图片需要将文件路径转换一下
     */
    handleInitValue: function(val) {
      const _this = this;
      // 找出所有img中的src
      const imgArray = val.match(imgReg) || [];
      const imgPaths = [];
      imgArray.forEach(img => {
        imgPaths.push(getValueFromSrc(img.match(srcReg)[0]));
      });
      if (imgArray.length === 0) {
        _this.myValue = val;
        return;
      }
      const imageIndex = imgArray.findIndex(item => item.includes("data:image"));
      const srcIndex = imgPaths.findIndex(item => item.includes("blob:http"));
      if (imageIndex !== -1 || srcIndex !== -1) {
        // 进入此 if 说明在富文本数据为空数据基础上直接上传图片，不需要转换路径
        return;
      }
      this.downloadImg(imgPaths)
        .then(() => {
          this.myValue = this.initModelImg(val);
        });
    },
    /**
     * 根据图片id集合 初始化所有图片
     */
    downloadImg(ids) {
      this.initImgs = ids;
      this.initImgsLength = ids.length;
      const arr = ids.map(id => this.getImgById(id));
      return Promise.all(arr);
    },
    /**
     * 第一次加载时对图片依次初始化
     */
    handleInitImg: function() {
      if (this.initImgs.length <= 0) {
        return false;
      }
      if (this.initImgsLength > 1) {
        this.initImgsLength -= 1;
      } else {
        this.initImgsLength = 0;
        this.handleBlur();
        this.handleReplaceInitImg();
      }
      return true;
    },
    /**
     * 将初始化的图片 id 和 url保存到 imgUrl 中用于保存时匹配替换
     */
    handleReplaceInitImg: function() {
      const initImgs = [...this.initImgs];
      setTimeout(() => {
        let len = 0;
        this.myValue.replace(/<img.*?src=['"](\S*?)['"].*?>/g, (a, b) => {
          if (!this.imgUrls[b] && initImgs[len]) {
            this.setImgCache(initImgs[len], b);
            len += 1;
          }
        });
      });
      this.initImgs = [];
    },
    /**
     * 用于失焦时获取最新数据（用于图片上传后双向绑定值存在缓存）
     */
    handleBlur: function() {
      this.myValue = window.tinymce.get(this.tinymceId).getContent();
    }
  }
};

</script>
<style>
/* 在el-dialog中tinymce z-index太小而被遮挡 */
.tox-tinymce-aux {
  z-index: 99999 !important;
}

.tinymce.ui.FloatPanel {
  z-index: 99;
}
</style>